Template Examples - Stepbar Navigation
Robert Wenzel, Engineer, Potix Corporation
April 18, 2017
ZK 8.0
Introduction
Sometimes we get asked does ZK have this component or that feature. In some cases those features are reasonable additions to the framework in other cases when the feature requested is highly individual it's not a suitable candidate for a general feature. This is especially true for layout intensive changes, where the actual code is little and the custom styling is dominating.
e.g. for navigation components
While the recipe might be different from time to time, ZK contains all the major ingredients to implement your requirements.
Modelling a simple navigation case requires a collection of navigable items and a selected item defining the current navigation target. I deliberately use abstract terms here since the idea is quite abstract itself. So far the idea is the same independent of how it's presented to the user.
An existing ZK API class matching our requirements is e.g. a ListModelList (which will come in handy in a few minutes).
By wrapping a ListModelList into our own application specific NavModel
class we can enrich it with our own functionalities for seamless integration into our use case.
The most important control methods are navigateTo(), getCurrent(), getItems()
, accompanied by a few derived navigation and state checking operations - next(), back(), isFirst(), isLast()
. For instant MVVM integration the class automatically invokes notify change when the current navigation item changes. Derived attributes using @DependsOn trigger a change-notification automatically.
public abstract class NavModel<NavType> {
private static final String CURRENT = "current";
private ListModelList<NavType> items = new ListModelList<>();
public void navigateTo(NavType item) {
items.addToSelection(item);
BindUtils.postNotifyChange(null, null, this, CURRENT);
}
public NavType getCurrent() {
Set<NavType> selection = items.getSelection();
return selection.iterator().next();
}
public void back() {
navigateTo(items.get(getCurrentIndex() - 1));
}
public void next() {
navigateTo(items.get(getCurrentIndex() + 1));
}
@DependsOn(CURRENT)
public boolean isFirst() {
return getCurrentIndex() == 0;
}
@DependsOn(CURRENT)
public boolean isLast() {
return getCurrentIndex() == items.size() - 1;
}
public ListModelList<NavType> getItems() {
return items;
}
private int getCurrentIndex() {
return items.indexOf(getCurrent());
}
}
Specialized StepBarModel
By substituting the generic type parameter <NavType>
with a concrete implementation class the model class above is capable to support various navigation scenarios.
e.g. a Step Bar (as in the screenshot below)
The additional information such as the label, icon, current state and the navigation target uri can be represented by a Step
class nested inside a StepBarModel
class (providing additional step bar specific navigation behavior overriding the navigateTo()
method).
public class StepBarModel extends NavModel<Step> {
@Override
public void navigateTo(Step step) {
//notify change all steps affected by random access navigation (steps between old and new index inclusive)
int oldIndex = getItems().indexOf(getCurrent());
int newIndex = getItems().indexOf(step);
super.navigateTo(step);
getItems().subList(Math.min(newIndex, oldIndex), Math.max(newIndex, oldIndex) + 1)
.forEach(affectedStep -> BindUtils.postNotifyChange(null, null, affectedStep, "*"));
}
public class Step {
private String label;
private String icon;
private String uri;
public Step(String label, String icon, String uri) {
super();
this.label = label;
this.icon = icon;
this.uri = uri;
}
public String getStatus() {
return isDone() ? "previous" : (getCurrent() == this ? "current" : "following");
}
public boolean isDone() {
return getItems().indexOf(this) < getItems().indexOf(getCurrent());
}
public String getLabel() {
return label;
}
public String getIcon() {
return icon;
}
public String getUri() {
return uri;
}
}
}
The Step
class itself is straight forward, simply providing information we need later when rendering the stepbar template.
Usage in HolidayOrderViewModel
Let's look into the actual usage in our View Model class which is equally trivial.
public class HolidayOrderViewModel {
private StepBarModel stepBarModel;
private boolean carAdded = false;
@Init
public void init() {
stepBarModel = new StepBarModel();
addStep("Destination", "z-icon-globe", "steps/destination.zul");
addStep("Accommodation", "z-icon-hotel", "steps/accommodation.zul");
addStep("Personal Details", "z-icon-user", "steps/personal-details.zul");
addStep("Payment", "z-icon-credit-card", "steps/payment.zul");
addStep("Enjoy Holiday", "z-icon-smile-o", "steps/finish.zul");
stepBarModel.getItems().addToSelection(stepBarModel.getItems().get(0));
}
@Command
public void gotoStep(@BindingParam("step") StepBarModel.Step step) {
stepBarModel.navigateTo(step);
}
@Command
public void next() {
stepBarModel.next();
}
@Command
public void back() {
stepBarModel.back();
}
...
public StepBarModel getStepBarModel() {
return stepBarModel;
}
...
public void addStep(String label, String icon, String uri) {
addStep(stepBarModel.getItems().size(), label, icon, uri);
}
public void addStep(int index, String label, String icon, String uri) {
stepBarModel.getItems().add(index, stepBarModel.new Step(label, icon, uri));
}
}
We initialize the StepBarModel
with the necessary navigation steps (each with: label, icon, uri).
The command handlers gotoStep(), next(), back()
delegate into the SideBarModel
providing the perfect hooks to add your application logic such as validation, loading/saving of related information from/to DB - allowing to block progress or skip steps if needed.
The omitted sections of the code snipped above contain additional commands to add or remove an optional step based on a user decision (read on).
Dynamic Model Changes
If a rented car is requested a "Rent Car" step is inserted into the current StepBarModel
(or removed if decided otherwise).
Adding removing of Steps is handled transparently by the backing ListModelList
providing data to render the navigation items - which brings us to the final Paragraph.
@Command
@NotifyChange("carAdded")
public void addCar() {
addStep(2, "Rent Car", "z-icon-car", "steps/rent-car.zul");
this.carAdded = true;
}
@Command
@NotifyChange("carAdded")
public void removeCar() {
Step carStep = stepBarModel.getCurrent();
stepBarModel.next();
stepBarModel.getItems().remove(carStep);
this.carAdded = false;
}
...
public boolean isCarAdded() {
return carAdded;
}
Render your Model
I saved the most fun part for the end... Now that our model objects provide streamlined information it requires only a few (even disappointingly few) lines of zul code to actually render the step bar. The <forEach>
shadow element does most of the job in combination with the ListModelList
keeping the contents of stepBarModel.items
in sync with the rendered step DIVs.
<div sclass="stepbar">
<forEach items="@init(stepBarModel.items)" var="step">
<div sclass="@load(('step ' += step.status))"
onClick="@command(step.done ? gotoStepCommand : null, step=step)">
<span sclass="@init(('step-icon ' += step.icon))"/>
<label sclass="step-label" value="@init(step.label)"/>
</div>
</forEach>
</div>
It can be embedded into a zul file e.g. by defining and using a custom <stepbar>
element as shown below.
<?component name="stepbar" templateURI="stepbar.zul"?>
<?style src="stepbar.css"?>
<zk>
...
<div viewModel="@id('vm') @init('zk.example.template.stepbar.HolidayOrderViewModel')"
validationMessages="@id('vmsgs')" style="padding: 0 200px" align="center">
<stepbar stepBarModel="@init(vm.stepBarModel)" gotoStepCommand="gotoStep"/>
<apply templateURI="@load(vm.stepBarModel.current.uri)" step="@load(vm.stepBarModel.current)"/>
<separator bar="true"/>
<div>
<if test="@load(!vm.stepBarModel.first and !vm.stepBarModel.last)">
<button onClick="@command('back')" label="Back"/>
</if>
<if test="@load(!vm.stepBarModel.last)">
<button onClick="@command('next')" label="Next"/>
</if>
</div>
</div>
</zk>
- Line 7: passing the template parameters
stepBarModel, gotoStepCommand
(as a callback command for clicked items) - Lines 9: examples of using model information such as the current uri to include the actual content for a step
- Lines 13: render/detach a button based on a model based condition
Styling
Of course the component wouldn't look as it looks without CSS styling (see: stepbar.css) ... however that's not the topic here and since styling is extremely individual I'll not go into details here. I as a coder simply chose something simple that looks OK-ish for this example. Your designers will have a different opinion of course and tell you what they want - then the template and associated styles might look more complex.
Here what I managed to fabricate ... what will you do? Let me know in the comments... :)
Summary
I hope this little example illustrates how careful model design enables both encapsulation of the details while providing an easy to use API for your application code. This enables you to implement the features you need in your application especially in areas where no standard solution exists. ZK components are not designed to represent your business logic but to compose a UI based on your business specific model classes.
Only a few powerful ZK features/components are required to build dynamic interactive UIs following the MVVM pattern - without accessing any ZK components in the View Model to provide a clean separation between the UI Components and the View Model.
Example Sources
The code examples are available on github in the zk-template-examples repository
- zul files: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/webapp/stepbar
- java classes: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/java/zk/example/template/stepbar
Running the Example
Clone the repo
git clone git@github.com:zkoss-demo/zk-template-examples.git
The example war file can be built using the gradle-wrapper (on windows simply omit the prefix './'):
./gradlew war
Execute using jetty:
./gradlew appRun
Then access the example http://localhost:8080/zk-template-examples/stepbar/
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |